/**
 * Anderground project - Subway maps processor application.
 * Copyright (C) 2008-2009 Eldar Abusalimov, Ilya Shurigyn
 * 
 * This file is part of Anderground.
 * 
 * Anderground is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * Anderground is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Anderground.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.anderground.core.model;

import java.io.IOException;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.anderground.core.util.Bindable;
import org.anderground.core.util.Binder;
import org.anderground.core.util.BindingException;
import org.anderground.core.util.Coord;
import org.anderground.core.util.Coordinated;
import org.anderground.core.util.Point;
import org.anderground.core.util.Time;

/**
 * Station entity class.
 * 
 * @author Ilya Shurigyn (honorik@gmail.com)
 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
 */
public class Station extends Bindable implements Coordinated, Serializable,
		Comparable<Station> {

	/**
	 * Describes how to show the stations label on the screen.
	 * 
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	public static class Label implements Serializable {

		/**
		 * @author Ilya Shurigyn
		 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
		 */
		public final class Editor {

			/**
			 * private constructor
			 */
			private Editor() {
			}

			/**
			 * @param angle
			 *            the angle to set
			 */
			public void setAngle(int angle) {
				Label.this.angle = angle;
			}

			/**
			 * @param visible
			 *            the visible to set
			 */
			public void setVisible(boolean visible) {
				Label.this.visible = visible;
			}

			/**
			 * @param offset
			 *            the offset to set
			 */
			public void setOffset(Point offset) {
				Label.this.offset = offset == null ? new Point() : offset;
			}

		}

		private static final long serialVersionUID = 5124687172759215176L;

		/** label's offset from the station center in screen pixels */
		private Point offset;

		/** the angle to rotate the label around the station center */
		private int angle;

		/** indicates the label's visibility */
		private boolean visible;

		transient private Editor editor;

		/**
		 * default constructor
		 */
		public Label() {
			this(0, null, true);
		}

		/**
		 * @param angle
		 *            - angle to rotate the label
		 * @param offset
		 *            - label's offset from the station center in screen pixels
		 * @param visible
		 *            - label visibility
		 */
		public Label(int angle, Point offset, boolean visible) {
			this.angle = angle;
			this.offset = offset == null ? new Point() : offset;
			this.visible = visible;
		}

		/**
		 * @return the offset
		 */
		public Point getOffset() {
			return offset;
		}

		/**
		 * @return the angle
		 */
		public int getAngle() {
			return angle;
		}

		/**
		 * @return the visible
		 */
		public boolean isVisible() {
			return visible;
		}

		/**
		 * @return
		 */
		Editor getEditor() {
			if (editor == null) {
				editor = new Editor();
			}

			return editor;
		}

		@Override
		public int hashCode() {
			return offset.hashCode() + angle * 17 + (visible ? 123 : 456);
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj) {
				return true;
			}

			Label another;
			if (obj instanceof Label) {
				another = (Label) obj;
			} else {
				return false;
			}

			if (angle != another.angle || visible != another.visible) {
				return false;
			}
			if (!offset.equals(another.offset)) {
				return false;
			}

			return true;
		}

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	public final class Editor {

		/**
		 * private constructor
		 */
		private Editor() {
		}

		/**
		 * @param name
		 *            the name to set
		 */
		public void setName(String name) {
			Station.this.name = name == null ? DEFAULT_NAME : name;
		}

		/**
		 * @param close
		 *            the closeTime to set
		 */
		public void setCloseTime(Time close) {
			Station.this.closeTime = close == null ? new Time() : close;
		}

		/**
		 * @param open
		 *            the openTime to set
		 */
		public void setOpenTime(Time open) {
			Station.this.openTime = open == null ? new Time() : open;
		}

		/**
		 * @param position
		 *            the position to set
		 */
		public void setPosition(Coord position) {
			Station.this.position = position == null ? new Coord() : position;
		}

		/**
		 * @param location
		 *            the location to set
		 */
		public void setLocation(Coord location) {
			Station.this.location = location == null ? new Coord() : location;
		}

	}

	private static final long serialVersionUID = -2716231540437767486L;

	public static final String DEFAULT_NAME = "Untitled Station";

	/** the name of the station */
	private String name;

	/** position on the screen */
	private Coord position;

	/** geographical coordinates */
	private Coord location;

	/** Schedule of working */
	private Time openTime, closeTime;

	/** station's label definition */
	private Label label;

	/** List of outgoing tunnels */
	transient private Set<Tunnel> tunnels;

	/** List of outgoing transfers */
	transient private Set<Transfer> transfers;

	/** List of all transitions either outgoing or incoming */
	transient private List<Transition> boundTransitions;

	/** List of lines to that this station's tunnels belong */
	transient private Set<Line> lines;

	/** the hub to that this station's each transfer belongs */
	transient private Hub hub;

	/** unmodifiable set backed by the {@link #transfers} set */
	transient private Set<Transfer> unmodifiableTransfersSet;

	/** unmodifiable set backed by the {@link #tunnels} set */
	transient private Set<Tunnel> unmodifiableTunnelsSet;

	transient private Editor editor;

	/**
	 * The constructor
	 */
	public Station() {
		this(DEFAULT_NAME, new Coord(), new Coord(), new Time(), new Time(),
				new Label());
	}

	/**
	 * The constructor
	 * 
	 * @param name
	 *            - station name
	 * @param position
	 *            - position on phone screen
	 * @param location
	 *            - location on the world map
	 * @param open
	 *            - open time
	 * @param close
	 *            - close time
	 * @param label
	 *            - describes how to draw the label
	 */
	public Station(String name, Coord position, Coord location, Time open,
			Time close, Label label) {
		init();
		this.name = name == null ? DEFAULT_NAME : name;
		this.position = position == null ? new Coord() : position;
		this.location = location == null ? new Coord() : location;
		this.openTime = open == null ? new Time() : open;
		this.closeTime = close == null ? new Time() : close;
		this.label = label == null ? new Label() : label;
	}

	/**
	 * Initializes some private fields.
	 */
	private void init() {
		tunnels = new HashSet<Tunnel>();
		transfers = new HashSet<Transfer>();
		unmodifiableTunnelsSet = Collections.unmodifiableSet(tunnels);
		unmodifiableTransfersSet = Collections.unmodifiableSet(transfers);
		boundTransitions = new LinkedList<Transition>();
		lines = new HashSet<Line>();
	}

	@Override
	protected void onUnbindFrom(Binder binder) throws BindingException {
		// The transition unbinding causes removing itself from the
		// boundTransitions list of its ends. Therefore we should create
		// a copy of boundTransitions to prevent it throwing
		// ConcurrentModificationException when iterating over its
		// elements.
		Transition[] transitions = (Transition[]) boundTransitions
				.toArray(new Transition[boundTransitions.size()]);
		for (Transition transition : transitions) {
			transition.unbindFrom(binder);
		}
	}

	@Override
	protected void onBindWith(Bindable bindable) throws BindingException {
		if (bindable instanceof Tunnel) {
			Tunnel tunnel = (Tunnel) bindable;
			if (tunnel.getFromStation() == this) {
				tunnels.add(tunnel);
			} else if (tunnel.getToStation() != this) {
				throw new BindingException("tunnel " + tunnel
						+ " must have the station " + this
						+ " as one of its ends");
			}

			lines.add(tunnel.getLine());

			boundTransitions.add(tunnel);

		} else if (bindable instanceof Transfer) {
			Transfer transfer = (Transfer) bindable;
			if (transfer.getFromStation() == this
					|| transfer.getToStation() == this) {
				if (hub != null) {
					hub.unbindFrom(getBinder());
				}
			} else {
				throw new BindingException("tunnel " + transfer
						+ " must have the station " + this
						+ " as one of its ends");
			}

			if (transfer.getFromStation() == this) {
				transfers.add(transfer);
			}

			boundTransitions.add(transfer);

		} else if (bindable instanceof Hub) {
			Hub hub = (Hub) bindable;
			if (this.hub != null) {
				throw new BindingException("station " + this
						+ " has already got a hub");
			}
			this.hub = hub;

		}
	}

	@Override
	protected void onUnbindWith(Bindable bindable) throws BindingException {
		if (bindable instanceof Tunnel) {
			Tunnel tunnel = (Tunnel) bindable;
			if (tunnel.getFromStation() == this) {
				tunnels.remove(tunnel);
			} else if (tunnel.getToStation() != this) {
				throw new BindingException("tunnel " + tunnel
						+ " must have the station " + this
						+ " as one of its ends");
			}

			boolean leaveLine = false;
			Line line = tunnel.getLine();
			for (Transition boundTransition : boundTransitions) {
				if (boundTransition != bindable
						&& boundTransition instanceof Tunnel) {
					if (((Tunnel) boundTransition).getLine() == line) {
						leaveLine = true;
						break;
					}
				}
			}
			if (!leaveLine) {
				if (!lines.remove(line)) {
					assert false;
				}
			}

			this.boundTransitions.remove(tunnel);

		} else if (bindable instanceof Transfer) {
			Transfer transfer = (Transfer) bindable;
			if (transfer.getFromStation() == this
					|| transfer.getToStation() == this) {
				if (hub != null) {
					hub.unbindFrom(getBinder());
				}
			} else {
				throw new BindingException("tunnel " + transfer
						+ " must have the station " + this
						+ " as one of its ends");
			}

			if (transfer.getFromStation() == this) {
				transfers.remove(transfer);
			}

			boundTransitions.remove(transfer);

		} else if (bindable instanceof Hub) {
			Hub hub = (Hub) bindable;
			if (this.hub != hub) {
				throw new BindingException("station " + this
						+ " has got another hub");
			}
			this.hub = null;

		}
	}

	/**
	 * Returns the list of all transitions bound with this station either
	 * outgoing or incoming.
	 * 
	 * @return the boundTransitions
	 */
	List<Transition> getBoundTransitions() {
		return boundTransitions;
	}

	/**
	 * Returns the hub field "as it is" without invoking
	 * {@link Binder#ensureConsistency()}.
	 * 
	 * @return the hub
	 */
	Hub getBoundHub() {
		return hub;
	}

	/**
	 * @return the name
	 */
	public String getName() {
		return name;
	}

	/**
	 * @return the openTime
	 */
	public Time getOpenTime() {
		return openTime;
	}

	/**
	 * @return the closeTime
	 */
	public Time getCloseTime() {
		return closeTime;
	}

	/**
	 * @return the position
	 */
	public Coord getPosition() {
		return position;
	}

	/**
	 * @return the location
	 */
	public Coord getLocation() {
		return location;
	}

	/**
	 * @return the unmodifiable set of the outgoing {@link Transfer}s
	 */
	public Set<Transfer> getTransfers() {
		return unmodifiableTransfersSet;
	}

	/**
	 * @return the unmodifiable set of the outgoing {@link Tunnel}s
	 */
	public Set<Tunnel> getTunnels() {
		return unmodifiableTunnelsSet;
	}

	/**
	 * lines getter helper
	 * 
	 * @return array of lines to that this station's each tunnel belongs
	 */
	public Line[] getLines() {
		return lines.toArray(new Line[lines.size()]);
	}

	/**
	 * hub getter helper
	 * 
	 * @return Hub containing the station or null if the station has no
	 *         transfers
	 */
	public Hub getHub() {
		if (isBound()) {
			getBinder().ensureConsistency();
			return hub;
		} else {
			return null;
		}
	}

	/**
	 * @return the label
	 */
	public Label getLabel() {
		return label;
	}

	/**
	 * @return
	 */
	Editor getEditor() {
		if (editor == null) {
			editor = new Editor();
		}

		return editor;
	}

	@Override
	public String toString() {
		return name;
	}

	/**
	 * Invoked when serializing this object.
	 * 
	 * @param out
	 * @throws IOException
	 */
	private void writeObject(java.io.ObjectOutputStream out) throws IOException {
		out.defaultWriteObject();
	}

	/**
	 * Invoked when deserializing this object.
	 * 
	 * @param in
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	private void readObject(java.io.ObjectInputStream in) throws IOException,
			ClassNotFoundException {
		in.defaultReadObject();
		init();
	}

	@Override
	public int compareTo(Station o) {
		return this.name.compareToIgnoreCase(o.name);
	}

	@Override
	public int hashCode() {
		if (isBound()) {
			return super.hashCode();
		} else {
			return name.hashCode() + position.hashCode() + location.hashCode()
					+ openTime.hashCode() + closeTime.hashCode();
		}
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}

		// separation of the cases whether the station is bound or not is a
		// consequence of using HashSets for storing stations in ModelData.
		if (isBound()) {
			// we assume that in one model there could be two different stations
			// with the same fields.
			// so we consider that the two different station objects represent
			// different stations.
			return false;
		}

		Station another;
		if (obj instanceof Station) {
			another = (Station) obj;
		} else {
			return false;
		}

		if (!name.equals(another.name)) {
			return false;
		}
		if (!position.equals(another.position)) {
			return false;
		}
		if (!location.equals(another.location)) {
			return false;
		}
		if (!openTime.equals(another.openTime)) {
			return false;
		}
		if (!closeTime.equals(another.closeTime)) {
			return false;
		}
		if (!label.equals(another.label)) {
			return false;
		}

		return true;
	}

}
